/*
 * Copyright 2012 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.drools.workbench.screens.guided.rule.client.widget;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.HasVerticalAlignment;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;
import org.drools.workbench.models.datamodel.oracle.DataType;
import org.drools.workbench.models.datamodel.oracle.FieldAccessorsAndMutators;
import org.drools.workbench.models.datamodel.oracle.MethodInfo;
import org.drools.workbench.models.datamodel.oracle.ModelField;
import org.drools.workbench.models.datamodel.rule.ExpressionFieldVariable;
import org.drools.workbench.models.datamodel.rule.ExpressionFormLine;
import org.drools.workbench.models.datamodel.rule.ExpressionMethod;
import org.drools.workbench.models.datamodel.rule.ExpressionMethodParameter;
import org.drools.workbench.models.datamodel.rule.ExpressionPart;
import org.drools.workbench.models.datamodel.rule.ExpressionText;
import org.drools.workbench.models.datamodel.rule.ExpressionUnboundFact;
import org.drools.workbench.models.datamodel.rule.ExpressionVariable;
import org.drools.workbench.models.datamodel.rule.FactPattern;
import org.drools.workbench.models.datamodel.rule.RuleModel;
import org.drools.workbench.screens.guided.rule.client.editor.ExpressionChangeEvent;
import org.drools.workbench.screens.guided.rule.client.editor.ExpressionChangeHandler;
import org.drools.workbench.screens.guided.rule.client.editor.ExpressionTypeChangeEvent;
import org.drools.workbench.screens.guided.rule.client.editor.ExpressionTypeChangeHandler;
import org.drools.workbench.screens.guided.rule.client.editor.HasExpressionChangeHandlers;
import org.drools.workbench.screens.guided.rule.client.editor.HasExpressionTypeChangeHandlers;
import org.drools.workbench.screens.guided.rule.client.editor.RuleModeller;
import org.drools.workbench.screens.guided.rule.client.resources.GuidedRuleEditorResources;
import org.gwtbootstrap3.client.ui.Button;
import org.gwtbootstrap3.client.ui.InputGroup;
import org.gwtbootstrap3.client.ui.InputGroupButton;
import org.gwtbootstrap3.client.ui.ListBox;
import org.gwtbootstrap3.client.ui.TextBox;
import org.kie.workbench.common.widgets.client.datamodel.AsyncPackageDataModelOracle;
import org.kie.workbench.common.widgets.client.resources.i18n.HumanReadableConstants;
import org.kie.workbench.common.widgets.client.widget.TextBoxFactory;
import org.uberfire.client.callbacks.Callback;
import org.uberfire.ext.widgets.common.client.common.ClickableLabel;
import org.uberfire.ext.widgets.common.client.common.SmallLabel;
import org.uberfire.ext.widgets.common.client.common.popups.FormStylePopup;

public class ExpressionBuilder extends RuleModellerWidget
        implements
        HasExpressionTypeChangeHandlers,
        HasExpressionChangeHandlers {

    private static final String DELETE_VALUE = "_delete_";
    private static final String TEXT_VALUE = "_text_";
    private static final String FIElD_VALUE_PREFIX = "fl";
    private static final String VARIABLE_VALUE_PREFIX = "va";
    // private static final String GLOBAL_COLLECTION_VALUE_PREFIX = "gc";
    private static final String GLOBAL_VARIABLE_VALUE_PREFIX = "gv";
    private static final String METHOD_VALUE_PREFIX = "mt";
    private final SmallLabelClickHandler slch = new SmallLabelClickHandler();
    private HorizontalPanel panel = new HorizontalPanel();
    private ExpressionFormLine expression;
    private boolean readOnly;

    private boolean isFactTypeKnown;

    public ExpressionBuilder( RuleModeller modeller,
                              EventBus eventBus,
                              ExpressionFormLine expression ) {
        this( modeller,
              eventBus,
              expression,
              false );
    }

    public ExpressionBuilder( RuleModeller modeller,
                              EventBus eventBus,
                              ExpressionFormLine expression,
                              Boolean readOnly ) {
        super( modeller,
               eventBus );
        this.expression = expression;

        if ( this.expression.isEmpty() ) {
            this.isFactTypeKnown = true;
        } else {
            this.isFactTypeKnown = getModeller().getDataModelOracle().isFactTypeRecognized( getModeller().getDataModelOracle().getFactNameFromType( this.expression.getRootExpression().getClassType() ) );
        }

        if ( readOnly == null ) {
            this.readOnly = !this.isFactTypeKnown;
        } else {
            this.readOnly = readOnly;
        }

        panel.setVerticalAlignment( HasVerticalAlignment.ALIGN_MIDDLE );
        panel.setStylePrimaryName( GuidedRuleEditorResources.INSTANCE.css().container() );

        initializeWidgets();

        initWidget( panel );
    }

    private void initializeWidgets() {
        panel.clear();
        StringBuilder bindingLabel = new StringBuilder();
        String binding = getBoundText();
        bindingLabel.append( "<b>" );
        bindingLabel.append( binding );
        bindingLabel.append( "</b>" );
        bindingLabel.append( ":" );

        if ( isExpressionEmpty() ) {
            if ( this.readOnly ) {
                panel.add( new SmallLabel( "<b>-</b>" ) );
            } else {
                panel.add( createStartPointWidget() );
            }

        } else {
            if ( this.readOnly ) {
                panel.add( createBindingWidgetForExpression( bindingLabel.toString() ) );
                panel.add( createWidgetForExpression() );
            } else {
                panel.add( createBindingWidgetForExpression( bindingLabel.toString() ) );
                panel.add( createWidgetForExpression() );
                panel.add( getWidgetForCurrentType() );
            }
        }
    }

    private boolean isExpressionEmpty() {
        return expression == null || expression.isEmpty();
    }

    private String getBoundText() {
        if ( expression.isBound() ) {
            return "[" + expression.getBinding() + "] ";
        }
        return "[not bound]";
    }

    private Widget createStartPointWidget() {
        ListBox startPoint = new ListBox();

        startPoint.addItem( GuidedRuleEditorResources.CONSTANTS.ChooseDotDotDot(),
                            "" );

        // TODO {baunax} uncomment when global collections is implemented.
        // for (String gc : getDataModelOracle().getGlobalCollections()) {
        // startPoint.addItem(gc, GLOBAL_COLLECTION_VALUE_PREFIX + "." + gc);
        // }

        for ( String gv : getDataModelOracle().getGlobalVariables() ) {
            startPoint.addItem( gv,
                                GLOBAL_VARIABLE_VALUE_PREFIX + "." + gv );
        }

        for ( String v : getRuleModel().getAllLHSVariables() ) {
            startPoint.addItem( v,
                                VARIABLE_VALUE_PREFIX + "." + v );
        }

        startPoint.setVisibleItemCount( 1 );
        startPoint.addChangeHandler( new ChangeHandler() {

            public void onChange( ChangeEvent event ) {
                ListBox lb = (ListBox) event.getSource();
                int index = lb.getSelectedIndex();
                if ( index > 0 ) {
                    onStartPointChange( lb.getValue( index ) );
                }
            }
        } );
        return startPoint;
    }

    private void onStartPointChange( final String value ) {
        setModified( true );
        panel.clear();
        final int dotPos = value.indexOf( '.' );
        final String prefix = value.substring( 0,
                                               dotPos );
        final String attrib = value.substring( dotPos + 1 );

        if ( prefix.equals( VARIABLE_VALUE_PREFIX ) ) {
            FactPattern fact = getRuleModel().getLHSBoundFact( attrib );
            ExpressionPart variable;
            if ( fact != null ) {
                variable = new ExpressionVariable( fact.getBoundName(),
                                                   fact.getFactType() );
            } else {
                //if the variable is not bound to a Fact Pattern then it must be bound to a Field
                String lhsBindingType = getRuleModel().getLHSBindingType( attrib );
                variable = new ExpressionFieldVariable( attrib,
                                                        lhsBindingType );
            }
            expression.appendPart( variable );
            onStartPointChangeUpdateWidget();

        } else if ( prefix.equals( GLOBAL_VARIABLE_VALUE_PREFIX ) ) {
            ExpressionPartHelper.getExpressionPartForGlobalVariable( getDataModelOracle(),
                                                                     attrib,
                                                                     new Callback<ExpressionPart>() {
                                                                         @Override
                                                                         public void callback( final ExpressionPart part ) {
                                                                             expression.appendPart( part );
                                                                             onStartPointChangeUpdateWidget();
                                                                         }
                                                                     } );
        }
    }

    private void onStartPointChangeUpdateWidget() {
        initializeWidgets();

        fireExpressionChangeEvent();
        fireExpressionTypeChangeEvent();
    }

    private Widget getWidgetForCurrentType() {
        if ( isExpressionEmpty() ) {
            return createStartPointWidget();
        }

        final ChangeHandler changeHandler = new ChangeHandler() {
            public void onChange( ChangeEvent event ) {
                ListBox box = (ListBox) event.getSource();
                panel.remove( box );
                if ( box.getSelectedIndex() > 0 ) {
                    onChangeSelection( box.getValue( box.getSelectedIndex() ) );
                }
            }
        };

        final ListBox listBox = new ListBox();
        listBox.addItem( GuidedRuleEditorResources.CONSTANTS.ChooseDotDotDot(),
                         "" );
        if ( includeDeleteOption() ) {
            listBox.addItem( "<==" + GuidedRuleEditorResources.CONSTANTS.DeleteItem(),
                             DELETE_VALUE );
        }
        listBox.addItem( "-- Text --",
                         TEXT_VALUE );

        getCompletionsForCurrentType( expression.getParts().size() > 1,
                                      new Callback<Map<String, String>>() {
                                          @Override
                                          public void callback( final Map<String, String> completions ) {
                                              for ( Map.Entry<String, String> entry : completions.entrySet() ) {
                                                  listBox.addItem( entry.getKey(),
                                                                   entry.getValue() );
                                              }
                                              listBox.addChangeHandler( changeHandler );
                                          }
                                      } );

        return listBox;
    }

    private boolean includeDeleteOption() {
        if ( expression.getParts().size() == 0 ) {
            return false;
        } else if ( expression.getParts().size() == 1 ) {
            if ( expression.getParts().get( 0 ) instanceof ExpressionUnboundFact ) {
                return false;
            }
        }
        return true;
    }

    private void onChangeSelection( String value ) {
        setModified( true );
        String prevFactName = null;
        final String oldType = getCurrentGenericType();

        if ( DELETE_VALUE.equals( value ) ) {
            expression.removeLast();
            onChangeSelectionUpdateExpressionWidget( oldType );

        } else if ( TEXT_VALUE.equals( value ) ) {
            expression.appendPart( new ExpressionText( "" ) );
            onChangeSelectionUpdateExpressionWidget( oldType );

        } else {
            int dotPos = value.indexOf( '.' );
            String prefix = value.substring( 0,
                                             dotPos );
            String attrib = value.substring( dotPos + 1 );

            prevFactName = getDataModelOracle().getFactNameFromType( getCurrentClassType() );
            if ( FIElD_VALUE_PREFIX.equals( prefix ) ) {
                ExpressionPartHelper.getExpressionPartForField( getDataModelOracle(),
                                                                prevFactName,
                                                                attrib,
                                                                new Callback<ExpressionPart>() {
                                                                    @Override
                                                                    public void callback( final ExpressionPart part ) {
                                                                        expression.appendPart( part );
                                                                        onChangeSelectionUpdateExpressionWidget( oldType );
                                                                    }
                                                                } );

            } else if ( METHOD_VALUE_PREFIX.equals( prefix ) ) {
                ExpressionPartHelper.getExpressionPartForMethod( getDataModelOracle(),
                                                                 prevFactName,
                                                                 attrib,
                                                                 new Callback<ExpressionPart>() {
                                                                     @Override
                                                                     public void callback( final ExpressionPart part ) {
                                                                         expression.appendPart( part );
                                                                         onChangeSelectionUpdateExpressionWidget( oldType );
                                                                     }
                                                                 } );
            }
        }
    }

    private void onChangeSelectionUpdateExpressionWidget( final String oldType ) {
        initializeWidgets();

        fireExpressionChangeEvent();
        fireExpressionTypeChangeEvent( oldType );
    }

    private void getCompletionsForCurrentType( final boolean isNested,
                                               final Callback<Map<String, String>> callback ) {
        if ( DataType.TYPE_FINAL_OBJECT.equals( getCurrentGenericType() ) ) {
            callback.callback( Collections.EMPTY_MAP );
            return;
        }

        final String factName = getDataModelOracle().getFactNameFromType( getCurrentClassType() );
        if ( factName != null ) {
            // we currently only support 0 param method calls
            getMethods( isNested,
                        callback,
                        factName );

        } else {
            // else {We don't know anything about this type, so return empty map}
            callback.callback( Collections.EMPTY_MAP );
        }
    }

    private void getMethods( final boolean isNested,
                             final Callback<Map<String, String>> callback,
                             final String factName ) {
        getDataModelOracle().getMethodInfos( factName,
                                             new Callback<List<MethodInfo>>() {
                                                 @Override
                                                 public void callback( final List<MethodInfo> methodInfos ) {
                                                     fillMethods( methodInfos,
                                                                  factName,
                                                                  isNested,
                                                                  callback );
                                                 }
                                             } );
    }

    private void fillMethods( final List<MethodInfo> methodInfos,
                              final String factName,
                              final boolean isNested,
                              final Callback<Map<String, String>> callback ) {
        getDataModelOracle().getFieldCompletions( factName,
                                                  FieldAccessorsAndMutators.ACCESSOR,
                                                  new Callback<ModelField[]>() {

                                                      @Override
                                                      public void callback( final ModelField[] fields ) {
                                                          //Use TreeMap so completions are sorted by methodNameWithParams
                                                          final Map<String, String> completions = new TreeMap<String, String>();

                                                          //Add fields
                                                          for ( ModelField field : fields ) {
                                                              final String fieldName = field.getName();
                                                              if ( !isNested || !fieldName.equals( DataType.TYPE_THIS ) ) {
                                                                  completions.put( fieldName,
                                                                                   FIElD_VALUE_PREFIX + "." + fieldName );
                                                              }
                                                          }
                                                          //Add methods
                                                          for ( MethodInfo methodInfo : methodInfos ) {
                                                              if ( !methodInfo.getGenericType().equals( DataType.TYPE_VOID ) ) {
                                                                  final String methodNameWithParams = methodInfo.getNameWithParameters();
                                                                  completions.put( methodNameWithParams,
                                                                                   METHOD_VALUE_PREFIX + "." + methodNameWithParams );
                                                              }
                                                          }
                                                          callback.callback( completions );
                                                      }
                                                  } );
    }

    private RuleModel getRuleModel() {
        return this.getModeller().getModel();
    }

    private AsyncPackageDataModelOracle getDataModelOracle() {
        return this.getModeller().getDataModelOracle();
    }

    private String getCurrentClassType() {
        return expression.getClassType();
    }

    private String getCurrentGenericType() {
        //If the last ExpressionPart is ExpressionText then we can't show any Fields or Methods from which to select
        if ( expression.getParts().isEmpty() ) {
            return null;
        }
        if ( expression.getParts().get( expression.getParts().size() - 1 ) instanceof ExpressionText ) {
            return DataType.TYPE_FINAL_OBJECT;
        }
        return expression.getGenericType();
    }

    private String getPreviousGenericType() {
        return expression.getPreviousGenericType();
    }

    @Override
    public boolean isReadOnly() {
        return this.readOnly;
    }

    @Override
    public boolean isFactTypeKnown() {
        return this.isFactTypeKnown;
    }

    /**
     * @see HasExpressionTypeChangeHandlers(ExpressionTypeChangeHandler)
     */
    public HandlerRegistration addExpressionTypeChangeHandler( ExpressionTypeChangeHandler handler ) {
        return addHandler( handler,
                           ExpressionTypeChangeEvent.getType() );
    }

    private void fireExpressionChangeEvent() {
        fireEvent( new ExpressionChangeEvent() );
    }

    private void fireExpressionTypeChangeEvent() {
        fireExpressionTypeChangeEvent( getPreviousGenericType() );
    }

    private void fireExpressionTypeChangeEvent( String previousGenericType ) {
        String currentGenericType = getCurrentGenericType();
        if ( ( previousGenericType == null || !previousGenericType.equals( currentGenericType ) ) || currentGenericType != null ) {
            fireEvent( new ExpressionTypeChangeEvent( previousGenericType,
                                                      currentGenericType ) );
        }
    }

    public HandlerRegistration addExpressionChangeHandler( ExpressionChangeHandler handler ) {
        return addHandler( handler,
                           ExpressionChangeEvent.getType() );
    }

    private void showBindingPopUp() {
        final FormStylePopup popup = new FormStylePopup( GuidedRuleEditorResources.CONSTANTS.ExpressionEditor() );
        final TextBox varName = new TextBox();
        if ( expression.isBound() ) {
            varName.setText( expression.getBinding() );
        }
        popup.addAttribute( GuidedRuleEditorResources.CONSTANTS.BindTheExpressionToAVariable(), new InputGroup() {{
            add( varName );
            add( new InputGroupButton() {{
                add( new Button( HumanReadableConstants.INSTANCE.Set() ) {{
                    addClickHandler( new ClickHandler() {
                        public void onClick( ClickEvent event ) {
                            String var = varName.getText();
                            if ( getModeller().isVariableNameUsed( var ) ) {
                                Window.alert( GuidedRuleEditorResources.CONSTANTS.TheVariableName0IsAlreadyTaken( var ) );
                                return;
                            }
                            expression.setBinding( var );
                            getModeller().refreshWidget();
                            popup.hide();
                        }
                    } );
                }} );
            }} );
        }} );
        popup.show();
    }

    private class SmallLabelClickHandler
            implements
            ClickHandler {

        public void onClick( ClickEvent event ) {
            showBindingPopUp();
        }
    }

    private ClickableLabel createBindingWidgetForExpression( final String text ) {
        ClickableLabel label = new ClickableLabel( text,
                                                   slch,
                                                   !this.readOnly );
        return label;
    }

    //Render Widgets for the Expression. ExpressionMethodParameter and ExpressionText parts
    //are represented by a TextBox to allow the User to edit the values, Updates are
    //reflected in the model.
    private Widget createWidgetForExpression() {
        final HorizontalPanel container = new HorizontalPanel();
        container.setVerticalAlignment( HasVerticalAlignment.ALIGN_MIDDLE );
        container.setStylePrimaryName( GuidedRuleEditorResources.INSTANCE.css().container() );
        for ( ExpressionPart expressionPart : expression.getParts() ) {
            if ( expressionPart instanceof ExpressionUnboundFact ) {
                continue;
            } else if ( this.readOnly ) {
                container.add( new Label( expressionPart.getName() ) );
            } else if ( expressionPart instanceof ExpressionMethod ) {
                container.add( new Label( expressionPart.getName() ) );
                container.add( new Label( "(" ) );
                final ExpressionMethod em = (ExpressionMethod) expressionPart;
                final List<ExpressionFormLine> emParams = em.getOrderedParams();
                for ( int index = 0; index < emParams.size(); index++ ) {
                    final ExpressionFormLine paramValueHolder = emParams.get( index );
                    final String paramDataType = em.getParameterDataType( paramValueHolder );
                    final ExpressionMethodParameter paramValue = ( (ExpressionMethodParameter) paramValueHolder.getRootExpression() );
                    final TextBox paramValueEditor = TextBoxFactory.getTextBox( paramDataType );
                    paramValueEditor.addValueChangeHandler( new ValueChangeHandler<String>() {
                        @Override
                        public void onValueChange( ValueChangeEvent<String> event ) {
                            paramValue.setText( event.getValue() );
                        }
                    } );
                    paramValueEditor.setText( paramValue.getName() );
                    container.add( paramValueEditor );
                    if ( index < emParams.size() - 1 ) {
                        container.add( new Label( ", " ) );
                    }
                }
                container.add( new Label( ")" ) );
            } else if ( !( expressionPart instanceof ExpressionText ) ) {
                container.add( new Label( expressionPart.getName() ) );
            } else {
                final TextBox tb = new TextBox();
                final ExpressionText expressionTextPart = (ExpressionText) expressionPart;
                tb.setText( expressionTextPart.getName() );
                tb.addChangeHandler( new ChangeHandler() {
                    @Override
                    public void onChange( final ChangeEvent changeEvent ) {
                        expressionTextPart.setText( tb.getText() );
                    }
                } );
                container.add( tb );
            }
            container.add( new Label( "." ) );
        }
        return container;
    }

}
